Skip to content

Batched (matrix) right-hand sides: LinearProblem(A, B) ≡ A \ B (v4.0.0)#1072

Merged
ChrisRackauckas merged 8 commits into
SciML:mainfrom
ChrisRackauckas-Claude:batch-rhs-v4
Jul 4, 2026
Merged

Batched (matrix) right-hand sides: LinearProblem(A, B) ≡ A \ B (v4.0.0)#1072
ChrisRackauckas merged 8 commits into
SciML:mainfrom
ChrisRackauckas-Claude:batch-rhs-v4

Conversation

@ChrisRackauckas-Claude

@ChrisRackauckas-Claude ChrisRackauckas-Claude commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Please ignore until reviewed by @ChrisRackauckas. Draft; opened by an agent at Chris's direction.

Closes #552.

Summary

solve(LinearProblem(A, B::AbstractMatrix)) and the init/solve! cache path now behave like A \ B, returning an n×k solution, for all factorization-based algorithms. Iterative/Krylov algorithms throw an informative ArgumentError at init time for matrix b instead of the previous internal MethodError (or silently wrong shapes).

Root cause of the old behavior: __init_u0_from_Ab(A, b) = similar(b, size(A, 2)) flattened a matrix b into a length-n vector u, so factorization solve! hit _ldiv!(::Vector, ::LU, ::Matrix) with no method.

What changed

  • src/common.jl: matrix-b method for __init_u0_from_Ab (zero-filled (size(A,2), size(b,2)), + SMatrix disambiguations); new _check_batched_rhs_support guard at __init for AbstractKrylovSubspaceMethod and Krylov-choosing DefaultLinearSolver.
  • src/factorization.jl: _check_residual_safety buffer made shape-generic.
  • src/default.jl: size heuristics use size(b, 1) instead of length(b) (3 sites) so batched b doesn't inflate the apparent problem size.
  • src/iterative_wrappers.jl: init_cacheval(::KrylovJL, A, b::AbstractMatrix, ...) = nothing so the default polyalgorithm's unused Krylov slots initialize for structured defaults.
  • src/mkl.jl / src/openblas.jl / src/appleaccelerate.jl: rectangular (m>n) solution copies made matrix-aware.
  • src/simplelu.jl: informative error (vector-only workspace).
  • ext/LinearSolveSparseArraysExt.jl: CHOLMOD pre-1.12 fallback widened to AbstractVecOrMat; SPQR matrix solve via \; SparseColumnPivotedQR column-by-column; size(b,1) heuristic.
  • test/Core/batch.jl (new, 85 tests, wired into Core), docs tutorial section + release note.
  • Project.toml: 3.87.0 → 4.0.0.

Batch support matrix (all verified by running)

Works (≡ A\B): LU, GenericLU (incl. BigFloat), QR (NoPivot/ColumnNorm), SVD, Cholesky (Symmetric/Hermitian), BunchKaufman, NormalCholesky, LDLt, Diagonal, DirectLdiv!, MKL/OpenBLAS/AppleAccelerate LU, UMFPACK, KLU, PureKLU, CHOLMOD, SparseColumnPivotedQR, the DEFAULT algorithm (dense/sparse/structured/non-square), static A+B, cache reuse (cache.A=, cache.b=), residual safety, singular-A Failure retcode.

Informative ArgumentError: all KrylovJL_*, SimpleGMRES, IterativeSolversJL/KrylovKitJL, SimpleLUFactorization, default→Krylov for operator A.

Left as-is: SMatrix A + heap Matrix B (StaticArrays.LU has no matrix ldiv!; SMatrix+SMatrix works); adjoint.jl matrix-b adjoints out of scope.

Why breaking (v4.0.0)

The u0 shape for matrix b changes from a flattened length-n vector to n×k, and matrix-b + iterative now errors at init. Downstream audit (OrdinaryDiffEq.jl, NonlinearSolve.jl): functionally unaffected — every call site passes _vec(...)/safe_vec-flattened vectors with explicit u0 (~60 sites audited in OrdinaryDiffEq libs, 1 production site in NonlinearSolveBase). They need only compat bumps for the major version (OrdinaryDiffEq: 10 lib Project.tomls at "3.75.0", RosenbrockTableaus "3.46", 2 test envs; NonlinearSolve: top-level + Base "3.48", FirstOrder/QuasiNewton "2.36.1, 3", gpu-test/docs envs).

Tests

GROUP=Core Pkg.test() on Julia 1.12.4 and 1.10.11: 1506 pass, 0 new failures (identical counts on both), including the 85 new batch tests. Pre-existing on unmodified main: 1 @test_broken in return codes, 2 in resize, and 1 error in forwarddiff_overloads.jl ("PureKLU direct dual path") — the latter bisected to aabf007 (#1069) and fixed separately in #1073.

Note: tests were run with the version field temporarily at 3.87.0 solely so temp test environments resolve; docs example manually verified equivalent to a passing test.

First adopter once released: phi!/PhiPadeCache in SciML/ExponentialUtilities.jl#236 (currently using lu!/ldiv! directly because the v3 cache path only solves vector b).

🤖 Generated with Claude Code

ChrisRackauckas and others added 3 commits July 3, 2026 11:42
`solve(LinearProblem(A, B))` with `B::AbstractMatrix` now computes the
equivalent of `A \ B`: `u0` is initialized as a `size(A, 2) × size(B, 2)`
matrix and the factorization-based algorithms solve all columns against a
single factorization of `A`. This is a breaking change (v4.0.0): previously a
matrix `b` initialized a flattened vector `u` and errored downstream.

- `__init_u0_from_Ab` gains matrix-`b` methods (including SMatrix
  disambiguation).
- Iterative/Krylov algorithms (`KrylovJL`, `SimpleGMRES`, and the other
  `AbstractKrylovSubspaceMethod`s, plus the default algorithm when it selects
  a Krylov method for an operator) throw an informative `ArgumentError` at
  `init` time for matrix `b`; `SimpleLUFactorization` (vector-only workspace)
  does the same.
- The default polyalgorithm's unused Krylov cacheval slots initialize to
  `nothing` for matrix `b` so structured-matrix defaults (Diagonal,
  SymTridiagonal, ...) work.
- `_check_residual_safety` sizes its residual buffer shape-generically.
- Default-algorithm size heuristics use `size(b, 1)` instead of `length(b)`
  so batched right-hand sides don't inflate the apparent problem size (same
  for the sparse `use_klulike_sparse_structure` heuristic).
- MKL/OpenBLAS/AppleAccelerate LU copy solution columns correctly for matrix
  `b` in the rectangular (`m > n`) path.
- SparseArrays extension: CHOLMOD pre-1.12 `_ldiv!` fallback widened to
  matrices, SPQR gains a matrix `_ldiv!` via `\` (no in-place matrix ldiv! on
  any Julia version), and SparseColumnPivotedQR solves matrix `b`
  column-by-column against the one factorization.

Part of SciML#552

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYp371jx6LurezUDhKcYRh
Covers dense LU/GenericLU/QR (NoPivot + ColumnNorm)/SVD/Cholesky/
BunchKaufman/NormalCholesky vs A \ B for Float64 and ComplexF64, the default
algorithm (dense, sparse, structured, non-square with k != n), sparse
UMFPACK/KLU/CHOLMOD and the non-square sparse QR default, cache reuse via
`cache.b = B2` and `cache.A = A2`, failure retcodes on singular A (including
the default's QR fallback), residual-safety with matrix B, informative errors
for Krylov methods and SimpleLUFactorization, static arrays, BigFloat, and
single-column-matrix vs vector consistency.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYp371jx6LurezUDhKcYRh
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYp371jx6LurezUDhKcYRh
All sublibrary, test, qa, GPU, Trim, and docs environments pinned
LinearSolve = "3[.x.y]", so every CI job died at resolution against the
4.0.0 bump ("empty intersection between LinearSolve@4.0.0 and project
compatibility"). Widen each to include 4. The docs environment also gains
[sources] entries for LinearSolve and LinearSolveAutotune (the registered
Autotune restricts LinearSolve to 3, so the docs build must use the local
copies, matching the pattern the test environments already use).

Verified locally: docs, lib/LinearSolveAutotune, and test/qa environments
all instantiate against 4.0.0.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ChrisRackauckas-Claude

Copy link
Copy Markdown
Contributor Author

CI fallout was the predictable major-bump resolution failure: 18 in-repo environments (sublibraries, test envs, qa, GPU, Trim, docs) pinned LinearSolve = "3", so every job died at instantiate with "empty intersection between LinearSolve@4.0.0 and project compatibility". Pushed 9c30317 widening all of them to include 4, plus [sources] entries in the docs env (registered LinearSolveAutotune restricts LinearSolve to 3, so docs must build against the local copies — same pattern the test envs already use). Verified locally that the docs, Autotune, and qa environments instantiate against 4.0.0.

The Runic check failure is pre-existing on main (test/Core/forwarddiff_overloads.jl landed unformatted in aabf007/#1069) — formatted in #1073 with the rest of that commit's fallout.

ChrisRackauckas and others added 2 commits July 3, 2026 14:25
The repo-wide Runic Format Check fails on main because this file landed
unformatted in aabf007 (SciML#1069); format it here alongside the dispatch fix
for the same commit's fallout.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every registered AlgebraicMultigrid >= 0.6 caps LinearSolve at 3, so the
root test target (which all CI test groups resolve) was unsatisfiable
against the 4.0.0 bump. AMG 0.5.x predates its LinearSolve dependency and
exposes the identical API surface the extension uses
(SmoothedAggregationAMG, RugeStubenAMG, aspreconditioner, ruge_stuben);
the AlgebraicMultigridJL testset passes on 0.5.1 (verified locally,
including the tight-tolerance case). The resolver will pick AMG 2.x again
automatically once AlgebraicMultigrid widens its own compat; the compat
comment marks the entry for removal then.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ChrisRackauckas-Claude

Copy link
Copy Markdown
Contributor Author

Two more CI fixes pushed (0c5e242, d64bad8):

  1. Runic: the format violation in test/Core/forwarddiff_overloads.jl is pre-existing on main (landed unformatted in aabf007/Route duals-in-b PureKLU solves to a native LinearCache (uses PureKLU 1.1 mixed ldiv!) #1069); the format fix is cherry-picked here too so this PR's check can go green independently of Fix PureKLU Dual-A dispatch stolen by DualBLinearProblem opt-out #1073.

  2. The real v4 bootstrap problem: every registered AlgebraicMultigrid ≥ 0.6 caps LinearSolve at 3, and AMG sits in the root test target that all CI test groups resolve — so one registry cap poisoned every job. Rather than dropping the AMG tests or pointing at a fork, root compat now also allows AlgebraicMultigrid = 0.5, which predates AMG's LinearSolve dependency but exposes the identical API the extension uses. Verified locally: resolver picks 0.5.1 under v4, the ext loads, and the full AlgebraicMultigridJL testset passes including the tight-tolerance case. Once AMG widens its compat (needs a PR to JuliaLinearAlgebra/AlgebraicMultigrid.jl — not opened, per repo-permission rules), the resolver auto-upgrades back to 2.x and the annotated compat entry can be dropped.

Registry scan says these are the only three packages in any in-repo environment that cap LinearSolve: AlgebraicMultigrid (handled above), and the two in-repo sublibraries LinearSolveAutotune/LinearSolvePyAMG (handled via widened compat + [sources]). Full root test target (23 extras) verified to resolve locally against 4.0.0.

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Contributor Author

Upstream compat PR opened at Chris's direction: JuliaLinearAlgebra/AlgebraicMultigrid.jl#135 (verified against this branch — AMG algorithms and the preconditioner path give bit-identical results under v3.87.0 and v4.0.0). Once that merges and tags, the temporary AlgebraicMultigrid = "0.5" compat entry here can be dropped and the resolver returns to AMG 2.x.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ChrisRackauckas-Claude

Copy link
Copy Markdown
Contributor Author

CI status: 61 pass, 1 fail — and the one failure ("Downgrade / Downgrade Tests - Core") is pre-existing infrastructure, not this branch:

  • Failure mode is "The self-hosted runner lost communication with the server" ~16–17 min into the test run (twice in a row on this PR, same spot), i.e. the runner process dies — no test assertion fails.
  • The same workflow fails on main: 4 of the last 5 main runs failed (2026-07-03 ×2, 2026-06-30, 2026-06-28; last success 2026-06-29). Whatever kills the runner predates this branch — the timing is consistent with something merged ~June 30 making the downgraded Core run heavy enough to starve the runner, but that's for a separate investigation.
  • This branch's Downgrade job passes resolution and build (the phases v4 could plausibly break, and the phases that did break before the compat fixes here); locally the downgraded env resolves.

So the v4 branch is green on everything that is green on main. Attempting a local repro of the downgraded Core run to see if it's an OOM; will file findings separately.

All in-repo environments and both registered sublibraries now require
LinearSolve 4: the environments always develop the local copy, and
v4-era releases of LinearSolveAutotune/LinearSolvePyAMG should require
the batched-RHS LinearSolve rather than straddle the major boundary.
Minor-bump both sublibraries (Autotune 1.12.0, PyAMG 1.2.0) for the
dependency floor raise. All environment classes re-verified to resolve.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ChrisRackauckas ChrisRackauckas marked this pull request as ready for review July 4, 2026 20:50
@ChrisRackauckas ChrisRackauckas merged commit 18f5b37 into SciML:main Jul 4, 2026
62 of 63 checks passed
ChrisRackauckas-Claude pushed a commit to ChrisRackauckas-Claude/ExponentialUtilities.jl that referenced this pull request Jul 4, 2026
With LinearSolve 4.0.0 supporting LinearProblem(A, B::Matrix) like A\B
(SciML/LinearSolve.jl#1072), PhiPadeCache now embeds a LinearSolve cache
and _phi_solve! runs the Pade denominator solve through it instead of
raw lu!/ldiv!. GenericLUFactorization is used because it refactorizes in
place with a cached pivot vector: workspace reuse is now fully
allocation-free (0 bytes measured for Float64 and ComplexF64 at
n = 7..100, p = 3..8, on Julia 1.10 and 1.12), improving on the lu!
version's per-call O(n) pivot allocation. (LUFactorization currently
copies A on every dense refactorization -- an in-place opt-in is being
added upstream, after which the LAPACK path can be swapped in for large
n.) Return-code semantics are unchanged: retcode Success -> info 0,
singular -> info 1 with NaN-filled outputs, non-finite results -> -1.

Tests tightened back to @allocated == 0; 254-assertion Phi testset
passes on 1.10 and 1.12 against LinearSolve 4.0.0.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYp371jx6LurezUDhKcYRh
ChrisRackauckas added a commit to SciML/NonlinearSolve.jl that referenced this pull request Jul 5, 2026
* Allow LinearSolve v4

LinearSolve v4.0.0 adds batched (matrix) right-hand side support
(SciML/LinearSolve.jl#1072). NonlinearSolve is functionally unaffected —
the one production solve site in NonlinearSolveBase passes a flattened
vector — so this only widens compat to include v4 and bumps affected
subpackage patch versions. (NonlinearSolveBase already at 2.31.4.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Re-trigger CI against LinearSolve v4 (now registered in General)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ChrisRackauckas added a commit to SciML/OrdinaryDiffEq.jl that referenced this pull request Jul 5, 2026
* Allow LinearSolve v4

LinearSolve v4.0.0 adds batched (matrix) right-hand side support
(SciML/LinearSolve.jl#1072). OrdinaryDiffEq is functionally unaffected —
all call sites pass flattened vectors with explicit u0 — so this only
widens compat to include v4 and bumps affected subpackage patch versions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Re-trigger CI against LinearSolve v4 (now registered in General)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ChrisRackauckas added a commit to SciML/ExponentialUtilities.jl that referenced this pull request Jul 5, 2026
…ix φ-functions (#236)

* Add O(p m^3) Al-Mohy--Liu scaling-and-recovering algorithm for phi functions

`phi(A, k)` / `phi!` previously computed phi_0(A), ..., phi_k(A) by calling
`phiv_dense!` on each of the m basis vectors, each exponentiating an
(m+k) x (m+k) block matrix -- overall O(m (m+k)^3).

This implements Algorithm 5.1 of Al-Mohy & Liu, "A scaling and recovering
algorithm for the matrix phi-functions" (arXiv:2506.01193, 2025), which
computes all phi functions simultaneously in O(k m^3): scale A by 2^-s,
evaluate the [m/m] diagonal Pade approximant to phi_p via Paterson--Stockmeyer,
recover the lower-index approximants with the recurrence
R^{(j)} = z R^{(j+1)} + 1/j!, and undo the scaling with the double-argument
formula. Backward-error-optimal Pade degree m and scaling s are selected from
the paper's theta table (Table 3.1) and the alpha_r / t cost analysis.

The new path is the default for Float64/ComplexF64 dense inputs (its Pade
tables are tuned for double precision); other element types and callers passing
the legacy `caches` bundle keep the basis-vector path. Diagonal inputs are
unchanged.

Validated against a high-precision block-exponential reference and the legacy
algorithm: worst-case relative error ~1e-14 across normal, non-normal,
large-norm, complex, Hessenberg and zero matrices for k = 1..10, matching or
beating the old accuracy. Measured speedups over the basis-vector path: 18x at
n=50, 51x at n=100, 72x at n=200 (k=3).

Closes #235.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYp371jx6LurezUDhKcYRh

* phi: reusable zero-alloc workspace + container-preserving static path

Address review feedback on #236:

* PhiPadeCache: a reusable workspace holding every scratch buffer. The
  strided Float64/ComplexF64 path (`_phi_almohy!`) now runs fully in place
  via mul!/getrf!/getrs! and broadcasting, allocating nothing once a cache
  exists (verified 0 bytes with @allocated across sizes). Pass it as the
  `caches` keyword to `phi!` to reuse across calls. (@stevengj: "do
  in-place, maybe?")

* Container-preserving generic path (`_phi_almohy_generic`) for immutable
  dense matrices: an SMatrix input stays an SMatrix throughout instead of
  being converted to a Matrix. Routed only for `!ismutable` inputs, so
  sparse/other mutable non-strided matrices keep the legacy path.
  (@stevengj: "unfortunate to use Matrix here if you start with SMatrix")

* Export and document PhiPadeCache and phi!; bump to 1.31.0 (new public
  API, SemVer minor).

Tests: workspace reuse + zero-allocation assertion, complex-matrix
workspace, and SMatrix static-in/static-out accuracy vs the block-exponential
reference. Full basictests + Aqua + JET pass locally (the only failures are
the pre-existing ForwardDiff-1.x Issue 42 asserts, green under CI's pinned
ForwardDiff 0.10.x).

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYp371jx6LurezUDhKcYRh

* phi: allocation-free LU across Julia versions (fixes LTS downgrade)

`LAPACK.getrf!(A, ipiv)` with a preallocated pivot vector only exists on
newer Julia; on the LTS (1.10) it errored, breaking the workspace solve.
ccall `getrf!` directly through libblastrampoline (as `StegrWork` does for
`stegr!`) so the factorization stays zero-allocation on all supported
versions; the 4-argument `getrs!` used for the solve is available
everywhere.

Verified on Julia 1.10 and 1.12: correctness ~2.5e-15 vs the BigFloat
reference and 0 bytes allocated on cache reuse.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYp371jx6LurezUDhKcYRh

* phi: ccall getrs! directly to keep the LU public-API-clean (QA)

The workspace solve used qualified `LinearAlgebra.BlasInt` and
`LinearAlgebra.LAPACK.getrs!`, which SciMLTesting's ExplicitImports
`all_qualified_accesses_are_public` check flags as reaching into non-public
internals (they are not on its ignore list). Mirror `exp_noalloc.jl`
instead: import `BlasInt` (an allow-listed explicit import) and ccall both
`getrf!` and `getrs!` through `BLAS.@blasfunc`/`BLAS.libblastrampoline`
(allow-listed names). No qualified access to non-public names remains in
`phi_almohy.jl`.

Behavior unchanged: verified correctness ~2.5e-15 and 0-byte cache reuse on
Julia 1.10 and 1.12; Aqua and JET still pass.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYp371jx6LurezUDhKcYRh

* phi: Horner PS, no-throw return codes, p-sized workspace, NaN guards

Re-analysis pass over the Al-Mohy--Liu implementation:

* Paterson--Stockmeyer rewritten in Horner form with the delta trick. The
  previous block scheme (mirroring the reference MATLAB) used explicit
  Atau powers plus two products per block -- up to 12 multiplications for
  m = 12 where Fasi's pi_m(tau) = 7, so the parameter selection was
  optimizing a cost the evaluator did not achieve. The Horner form's count
  now equals pi_m(tau) exactly. Measured 10-20% end-to-end speedup on the
  workspace path across n = 25..200.

* Numerical failure returns codes instead of throwing, so adaptive
  integrators can reject a step rather than abort: cache.info[] is 0 on
  success, the LAPACK getrf info for a singular Pade denominator, or -1
  for non-finite results (LAPACK does not flag NaN pivots, so a post-solve
  finiteness check backs the code); outputs are NaN-filled on failure.
  Fixes a latent InexactError where NaN/Inf input crashed in
  ceil(Int, t) during parameter selection before reaching the solve, and
  the generic path's SingularException from `\`. Wrongly sized workspaces
  (programmer errors) still throw, with informative messages.

* Workspace vectors sized from p at construction (was a fixed cap that
  BoundsError'd for p >~ 48) with an explicit capacity check; opnorm(A, 1)
  hoisted out of the per-degree ell loop (was recomputed 8x); dead Atau/Id
  buffers dropped (2n^2 less memory).

* Fix a closure-capture leak in the test helpers: phi_block_reference
  assigned to `n`, clobbering the enclosing testset's `n` (Julia closures
  assign to existing outer locals); the helpers now declare their
  temporaries `local`.

Verified: worst relative error 8.8e-15 over 33 BigFloat-referenced cases
(real/complex, normal/nonnormal, p = 1..8); 0 bytes allocated on cache
reuse (Float64 and ComplexF64, n = 10..100, p = 1..8, Julia 1.10 and
1.12); NaN/Inf inputs return info != 0 with NaN outputs and no throw;
full basictests 574 pass on 1.10 and 1.12 (only the pre-existing
ForwardDiff-1.x Issue 42 failures remain, which pass under CI's pinned
ForwardDiff 0.10); ExplicitImports clean.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYp371jx6LurezUDhKcYRh

* phi: overflow-safe backward-error coefficient; public lu! replaces ccalls

Two review-driven changes:

* _phi_be_coeff evaluated (m+p)!, m!, (2m+p)!, (2m+p+1)! separately before
  dividing, so intermediates could spuriously overflow (Inf/Inf = NaN for
  p >~ 159) even where the quotient is representable (@stevengj). Rewritten
  as a product of sub-unit factors,
  prod 1/(m+p+j) * prod 1/(m+j), which is monotonically decreasing and can
  only underflow when the true value does. Matches the BigFloat-exact value
  to 1.1e-15 over m = 1:12, p = 1:60; new p = 30 regression test.

* The hand-rolled getrf!/getrs! ccalls are gone: the solve now uses the
  public LinearAlgebra API, lu!(Dfact; check = false) + issuccess + ldiv!,
  which supports the matrix RHS directly and keeps the no-throw return-code
  semantics (issuccess false -> info > 0, NaN-filled outputs).
  LinearSolve.jl was considered but its LinearCache only solves vector
  right-hand sides through the public path, which would force a
  column-by-column loop or reaching into cache internals; plain lu!/ldiv!
  achieves ccall-free with zero new dependencies. Cost: the pivot vector is
  now the one per-call allocation (8n + ~100 bytes, measured 112 B at
  n = 7 to 928 B at n = 100; all O(n^2) buffers still reused), since no
  public API preallocates lu! pivots. Allocation tests assert that O(n)
  bound. The BlasInt import and ipiv field are removed; phi_almohy.jl is
  now entirely public-API.

Verified on Julia 1.10 and 1.12: 254-assertion Phi testset passes
(including new large-p and allocation-bound tests), full basictests 605
pass (only the pre-existing ForwardDiff-1.x Issue 42 failures remain),
ExplicitImports clean, NaN/Inf return codes intact.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYp371jx6LurezUDhKcYRh

* phi: solve through LinearSolve v4 batched matrix RHS

With LinearSolve 4.0.0 supporting LinearProblem(A, B::Matrix) like A\B
(SciML/LinearSolve.jl#1072), PhiPadeCache now embeds a LinearSolve cache
and _phi_solve! runs the Pade denominator solve through it instead of
raw lu!/ldiv!. GenericLUFactorization is used because it refactorizes in
place with a cached pivot vector: workspace reuse is now fully
allocation-free (0 bytes measured for Float64 and ComplexF64 at
n = 7..100, p = 3..8, on Julia 1.10 and 1.12), improving on the lu!
version's per-call O(n) pivot allocation. (LUFactorization currently
copies A on every dense refactorization -- an in-place opt-in is being
added upstream, after which the LAPACK path can be swapped in for large
n.) Return-code semantics are unchanged: retcode Success -> info 0,
singular -> info 1 with NaN-filled outputs, non-finite results -> -1.

Tests tightened back to @allocated == 0; 254-assertion Phi testset
passes on 1.10 and 1.12 against LinearSolve 4.0.0.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYp371jx6LurezUDhKcYRh

* Retrigger CI: LinearSolve 4.1.0 registered

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* exp_generic: fix dropped Dual partials at x = 0 under ForwardDiff >= 1

ForwardDiff 1.0 made iszero(::Dual) require zero partials as well, so
Dual(0, 1) fell through to intlog2, where ceil(Int, nx) dropped the
partials and 2^intlog2(0) = 2^64 overflowed to 0; the final
result^0 = one(result) then returned derivative 0 instead of 1 (Issue
42 testset). Gate the scaling on nx > 1 instead: values in (0, 1]
already produced s = 0, and Dual comparison uses the value only, so
zero-valued Duals with nonzero partials now take the s = 0 branch.
Verified against both ForwardDiff 0.10 and 1.x.

Widens the ForwardDiff test compat to "0.10.13, 1" accordingly; with
LinearSolve 4 in the dependency tree, PureKLU 1.1 forces ForwardDiff
>= 1, which is how this latent failure surfaced.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: ChrisRackauckas-Claude <accounts@chrisrackauckas.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Batch mode

2 participants